/* 
 * ClockView.m, a simple clock view 
 * Author: Ali T. Ozer, NeXT Developer Support Group
 * Created: May 26, 1989 (for version 0.9)
 * Modified: June 14 and Aug 14, 1989 (for version 1.0)
 * Redesigned for 2.0 by Julie Zelenski, NeXT Developer Support
 * Modified some for 3.0 by Ali Ozer, AppKit Group, May 28, 1992
 *
 * Subclass of view to implement a simple clock. This view is pretty generic 
 * and can probably be added to any program. The setClockType: method lets 
 * you display an analog, digital, or sundial face.  You have the option of 
 * turning the seconds hand on or off, as well as controlling whether the date 
 * is also displayed.  
 *  
 * You may freely copy, distribute and reuse the code in this example.  
 * NeXT disclaims any warranty of any kind, expressed or implied, as to its 
 * fitness for any particular use.
 */

#import <appkit/appkit.h>
#import "ClockView.h"
#import "Clock.h"	// PSwrap routines
#import <objc/NXStringTable.h>
#import <string.h>
#import <sys/time.h>	


@implementation ClockView:View


#define PI (double)3.1415926535897
	
#define ANALOG	0	
#define DIGITAL	1
#define SUNDIAL 2



/* ShowTime() is the timed entry function called by the
 * timed entry mechanism. It first writes the time out on the face 
 * of the clock, and then reinstalls the timed entry if the clock is 
 * not showing the seconds. If the seconds are being shown, no need to 
 * reinstall the timed entry; a second skipped here and then won't matter. 
 * If minutes are being shown, we want to make sure that the minute jumps 
 * at the next top of the minute, regardless of how long it took to service 
 * the timed entry.
 */
void ShowTime (teNum, now, clock) 
DPSTimedEntry teNum;
double now;
id clock;
{
    [clock display];    
    if ([clock showSeconds] == NO) [[clock stopTimedEntry] startTimedEntry:NO];
}



 
- initFrame:(const NXRect *)frameRect
/* initFrame for newly created view, initializes the various parameters,
 * grabs some fonts to be used later.
 */
{
    [super initFrame:frameRect];

    face = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
    [face useDrawMethod:@selector(drawFace:) inObject:self];

    littleFont = [Font newFont:"Helvetica" size:12 style:0
    			matrix:NX_IDENTITYMATRIX];
    mediumFont = [Font newFont:"Times-Roman" size:14 style:0
    			matrix:NX_IDENTITYMATRIX];
    bigFont = [Font newFont:"Times-Roman" size:24 style:0
    			matrix:NX_IDENTITYMATRIX];

    /* Set the default state (analog face, no seconds, date on) */
    clockType = ANALOG;
    showSeconds = NO;
    showDate = YES;
    center.x = bounds.size.width/2.0;
    center.y = bounds.size.height/2.0 + [mediumFont pointSize]/2.0;
    radius = MIN(center.x,center.y-[mediumFont pointSize]);
    [face recache];
    
    /* Start the time entry. YES indicates that this is the first time */
    [self startTimedEntry:YES];
    [self display];
    return self;
}


- free
/* Good idea to get rid of the timed entry while freeing... 
 */
{
    [face free];
    [self stopTimedEntry];
    return [super free];
}


/* SET/GET CLOCK PARAMETERS */

- setShowSeconds:(BOOL)newValue
/* setShowSeconds: sets whether or not the seconds hand is shown.
 * The timed entry must be reinstalled whenever this setting is changed
 * as time to the next firing changes.
 */
{ 
    showSeconds = newValue;
    [[self stopTimedEntry] startTimedEntry:NO];
    [self display];
    return self;
}

- setShowDate:(BOOL)newValue
/* setShowDate: sets whether or not the date is shown.
 */
{ 
    showDate = newValue;
    [self display];
    return self;
}

- setClockType:(int)newValue
/* setClockType: sets which type of clock is drawn (analog, digital, or 
 * sundial).
 */
{
    clockType = newValue;
    [face recache];
    [self display];
    return self;
}

- (BOOL)showSeconds 
{ 
    return showSeconds; 
}
- (BOOL)showDate
{ 
    return showDate; 
}
- (int)clockType
{
    return clockType;
}


/* TARGET/ACTION METHODS */

- changeShowDate:sender;
{
     return [self setShowDate:[[sender selectedCell] state]];
}
- changeShowSeconds:sender
{
    return [self setShowSeconds:[[sender selectedCell] state]];
}
- changeClockType:sender
{
    return [self setClockType:[sender selectedTag]];
}

/* String keys used for look-up into string tables */

static const char *monthKeys[12] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
static const char *months[12];
static const char *weekKeys[7] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
static const char *weekdays[7];
static const char *romanDays[31] = {"I","II","III","IV","V","VI","VII","VIII","IX","X","XI","XII",
"XIII","XIV","XV","XVI","XVII","XVIII","XIX","XX","XXI","XXII","XXIII",
"XXIV","XXV","XXVI","XXVII","XXVIII","XXIX","XXX", "XXXI"};

- setSTable:anObject;
/* Get string values for months, weekdays from NXStringTable, they are stored
 * as static class variables.
 */  
{
    int i;
    
    sTable = anObject;
    for (i = 0; i < 7; i++)
         weekdays[i] = [sTable valueForStringKey:weekKeys[i]];
    for (i = 0; i < 12; i++) 
         months[i] = [sTable valueForStringKey:monthKeys[i]];
    return self;
}

/* PRIVATE METHODS */

#define HOURRATIO	0.5	/* Hour hand length compared face size */
#define MINUTERATIO	0.85	/* Minute & seconds hands */

- drawAnalog:(struct tm *)time;
/* drawAnalog draws the clock hands and date for the analog clock face
 */
{
    int min,hour,sec;   
    char dateString[15];

    min  = time->tm_min;
    hour = time->tm_hour; 
    sec  = time->tm_sec;
    
    if (showSeconds) 
        PSWdrawClockHand (center.x,center.y,-6.0 * sec, radius*MINUTERATIO, 
			NX_DKGRAY, 0.0);
    PSWdrawClockHand (center.x,center.y, -(hour+min/60.0) * 30.0, 
    		radius*HOURRATIO, NX_BLACK, 1.0);
    PSWdrawClockHand (center.x,center.y,- fmod(min,60.0) * 6.0, 
    		radius*MINUTERATIO, NX_BLACK, 1.0);
    if (showDate) {
        [littleFont set];
	sprintf(dateString,"%s %s %d",weekdays[time->tm_wday],
				months[time->tm_mon],
				time->tm_mday);
	PSWcenterShow(center.x+1.0,3.0,dateString,NX_WHITE);
	PSWcenterShow(center.x,3.0,dateString,NX_DKGRAY);
    }
    return self;
}

- drawDigital:(struct tm *)time;
/* drawDigital draws the time and date for the digital clock face
 */
{
    int hour;
    char timeString[10];
    char dateString[15];
    
    hour = fmod(time->tm_hour,12); /* get us off military time */
    if (!hour) hour = 12;  /* if noon or midnight */
    if (showSeconds)
        sprintf(timeString,"%d:%.2d:%.2d",hour,
    					time->tm_min,
    					time->tm_sec);
    else 
        sprintf(timeString,"%d:%.2d", hour,time->tm_min);
    [bigFont set];
    PSWcenterShow(center.x,center.y-8.0,timeString,NX_BLACK);
    if (showDate) {
	sprintf(dateString,"%s %s %d",weekdays[time->tm_wday],
				months[time->tm_mon],
				time->tm_mday);
	[mediumFont set];
	PSWcenterShow(center.x,center.y-24.0,dateString,NX_BLACK);
    }
    return self;
}

#define SHADOWRATIO .95		/* shadow length when compared to radius */
#define MARKERRATIO .15		/* height of marker when compared to radius */

- drawSundial:(struct tm *)time;
/* drawSundial draws the shadow and date for the sundial clock face
 */
{   
    float percentOfDay;
    char dateString[15];
    NXPoint edge;

    if (showSeconds) 
        PSWdrawSweep (center.x,center.y,-6.0 *time->tm_sec,radius*.75);
    percentOfDay = (time->tm_hour*60 + time->tm_min)/(24.0*60.0);
    edge.x  = sin(percentOfDay*PI*2)*radius*SHADOWRATIO;
    edge.y = cos(percentOfDay*PI*2)*radius*SHADOWRATIO;
    PSWdrawShadow(center.x,center.y,edge.x,edge.y,radius*MARKERRATIO);
    if (showDate) {
	[mediumFont set];
	sprintf(dateString,"%s %s", months[time->tm_mon],
			romanDays[time->tm_mday-1]);
	PSWcenterShow(center.x,12.0,dateString,NX_BLACK);
    }
    return self;
}

- drawFace:image
/* drawFace draws the clock face image.  This
 * image is composited on screen and then the hands, shadow,
 * whatever is drawn on top of the face for the current time.
 */
{
    PSsetgray (NX_LTGRAY);
    NXRectFill (&bounds);	// Erase background
    switch (clockType) {
        case ANALOG: 	PSWdrawAnalogFace(center.x,center.y,radius);
			break;
	case DIGITAL: 	/* digital "face" is just blank */
			break;
	case SUNDIAL: 	PSWdrawSundialFace(center.x,center.y, radius);
			break;
    }
    return self;
}


- drawSelf:(NXRect *)rects :(int)rectCount
/* Draws face and hands of clock.
 * The clock face image in face is copied into the bounds of the 
 * view, and a routine is called to display the current date and time
 */ 
{	
    struct tm *localTime;
    struct timeval currentTime;

    // If recache was called, or if printing, this will first redraw
    // the clock face by calling drawFace:
    [face composite:NX_COPY toPoint:&bounds.origin];

    gettimeofday (&currentTime, NULL);
    localTime = localtime (&(currentTime.tv_sec));
    switch (clockType) {
        case ANALOG: 	[self drawAnalog:localTime];
			break;
	case DIGITAL: 	[self drawDigital:localTime];
			break;
	case SUNDIAL: 	[self drawSundial:localTime];
			break;
    }
    return self;
}


- startTimedEntry:(BOOL)fireASAP
/* startTimedEntry will install the timed entry. If fireASAP is YES, the
 * timed entry is set to fire off as soon as possible (this would be the case
 * at the start of the program, for instance). If fireASAP is NO, then the
 * timed entry is set to fire off in one second (if seconds are being shown)
 * or at the top of the next minute (in anytime between 0 and 60 seconds).
 */
{
    double fireIn;

    if (fireASAP) fireIn = 0.0;		  // Fire as soon as possible!
    else if (showSeconds) fireIn = 1.0;	  // Fire in a second (good enough)
    else {
        struct timeval currentTime;
    	gettimeofday (&currentTime, NULL);
	fireIn = 60.0 - (currentTime.tv_sec % 60);  // Top of the minute
    }
	
    teNum = DPSAddTimedEntry(fireIn, &ShowTime, self, NX_MODALRESPTHRESHOLD);
    return self;
}

- stopTimedEntry
/* Removes the clock timed entry.
 */
{
    if (teNum)
         DPSRemoveTimedEntry (teNum);
    teNum = (DPSTimedEntry)0;
    return self;
}


- sizeTo:(NXCoord)w :(NXCoord)h
/* Overriding sizeTo:: allows us to resize and fix up the clock whenever
 * the size is changed.  Figure the radius of the clock (based on the size
 * of the view) and then redraw
 */
{
    [super sizeTo:w :h];
    center.x = bounds.size.width/2.0;
    center.y = bounds.size.height/2.0 + [mediumFont pointSize]/2.0;
    radius = MIN(center.x, center.y-[mediumFont pointSize]);
    [face setSize:&bounds.size];
    return self;
}

@end